/*
 * Copyright 2014 NAVER Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.navercorp.pinpoint.profiler.receiver.service;

import com.navercorp.pinpoint.common.util.ThreadMXBeanUtils;
import com.navercorp.pinpoint.profiler.receiver.CommandSerializer;
import com.navercorp.pinpoint.profiler.receiver.ProfilerRequestCommandService;
import com.navercorp.pinpoint.profiler.receiver.ProfilerStreamCommandService;
import com.navercorp.pinpoint.rpc.packet.stream.StreamCode;
import com.navercorp.pinpoint.rpc.stream.ServerStreamChannel;
import com.navercorp.pinpoint.rpc.stream.ServerStreamChannelContext;
import com.navercorp.pinpoint.rpc.stream.StreamChannelStateChangeEventHandler;
import com.navercorp.pinpoint.rpc.stream.StreamChannelStateCode;
import com.navercorp.pinpoint.rpc.util.TimerFactory;
import com.navercorp.pinpoint.thrift.dto.command.*;
import com.navercorp.pinpoint.thrift.util.SerializationUtils;

import org.apache.thrift.TBase;
import org.jboss.netty.util.HashedWheelTimer;
import org.jboss.netty.util.Timeout;
import org.jboss.netty.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xerial.snappy.Snappy;

import java.lang.management.LockInfo;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author koo.taejin
 */
public class ThreadDumpService implements ProfilerRequestCommandService, ProfilerStreamCommandService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final ThreadDumpStreamChannelStateChangeEventHandler stateChangeEventHandler = new ThreadDumpStreamChannelStateChangeEventHandler();
    private final List<ServerStreamChannel> streamChannelRepository = new CopyOnWriteArrayList<ServerStreamChannel>();
    private final AtomicBoolean onTimerTask = new AtomicBoolean(false);
    private final HashedWheelTimer timer = TimerFactory.createHashedWheelTimer("ThreadDumpService-Timer", 100, TimeUnit.MILLISECONDS, 512);
    private final long flushDelay;
    private static final long DEFAULT_FLUSH_DELAY = 3000;
    private TBase requesttbase;
    private int runnum;
    
    public ThreadDumpService() {
    	flushDelay = DEFAULT_FLUSH_DELAY;
    }
    
    private final Object lock = new Object();
    @Override
    public TBase<?, ?> requestCommandService(TBase tbase) {
        logger.info("{} execute {}.", this, tbase);

        TCommandThreadDump param = (TCommandThreadDump) tbase;
        TThreadDumpType type = param.getType();

        long t0 = System.currentTimeMillis();
        List<ThreadInfo> threadInfoList = null;
        if (TThreadDumpType.TARGET == type) {
            threadInfoList = getThreadInfo(param.getName());
        } else if (TThreadDumpType.PENDING == type) {
            threadInfoList = getThreadInfo(param.getPendingTimeMillis());
        } else {
            threadInfoList = Arrays.asList(getAllThreadInfo());
        }
        
        TCommandThreadDumpResponse response = new TCommandThreadDumpResponse();

        for (ThreadInfo info : threadInfoList) {
            TThreadDump dump = new TThreadDump();
            
            dump.setThreadName(info.getThreadName());
            dump.setThreadId(info.getThreadId());
            dump.setBlockedTime(info.getBlockedTime());
            dump.setBlockedCount(info.getBlockedCount());
            dump.setWaitedTime(info.getWaitedTime());
            dump.setWaitedCount(info.getWaitedCount());

            dump.setLockName(info.getLockName());
            dump.setLockOwnerId(info.getLockOwnerId());
            dump.setLockOwnerName(info.getLockOwnerName());

            dump.setInNative(info.isInNative());
            dump.setSuspended(info.isSuspended());

            dump.setThreadState(getThreadState(info));

            StackTraceElement[] stackTraceElements = info.getStackTrace();
            for (StackTraceElement each : stackTraceElements) {
                dump.addToStackTrace(each.toString());
            }

            MonitorInfo[] monitorInfos = info.getLockedMonitors();
            for (MonitorInfo each : monitorInfos) {
                TMonitorInfo tMonitorInfo = new TMonitorInfo();

                tMonitorInfo.setStackDepth(each.getLockedStackDepth());
                tMonitorInfo.setStackFrame(each.getLockedStackFrame().toString());

                dump.addToLockedMonitors(tMonitorInfo);
            }

            LockInfo[] lockInfos = info.getLockedSynchronizers();
            for (LockInfo lockInfo : lockInfos) {
                dump.addToLockedSynchronizers(lockInfo.toString());
            }
            response.addToThreadDumps(dump);
        }
        logger.info("requestCommandService time:{}", System.currentTimeMillis()-t0);
        return response;
    }

    private TThreadState getThreadState(ThreadInfo info) {

        String stateName = info.getThreadState().name();

        for (TThreadState state : TThreadState.values()) {
            if (state.name().equalsIgnoreCase(stateName)) {
                return state;
            }
        }

        return null;
    }

    private List<ThreadInfo> getThreadInfo(String threadName) {
        List<ThreadInfo> result = new ArrayList<ThreadInfo>();

        if (threadName == null || threadName.trim().equals("")) {
            return Arrays.asList(getAllThreadInfo());
        }

        for (ThreadInfo threadIno : getAllThreadInfo()) {
            if (threadName.equals(threadIno.getThreadName())) {
                result.add(threadIno);
            }
        }

        return result;
    }

    // TODO : need to modify later
    private List<ThreadInfo> getThreadInfo(long pendingTimeMillis) {
        List<ThreadInfo> result = new ArrayList<ThreadInfo>();

        if (pendingTimeMillis <= 0) {
            return Arrays.asList(getAllThreadInfo());
        }

        for (ThreadInfo threadInfo : getAllThreadInfo()) {
            if (threadInfo.getBlockedTime() >= pendingTimeMillis) {
                result.add(threadInfo);
                continue;
            }

            if (threadInfo.getWaitedTime() >= pendingTimeMillis) {
                result.add(threadInfo);
            }
        }

        return result;
    }

    private ThreadInfo[] getAllThreadInfo() {
        ThreadInfo[] threadInfos = ThreadMXBeanUtils.dumpAllThread();

        return threadInfos;
    }

    @Override
    public Class<? extends TBase> getCommandClazz() {
        return TCommandThreadDump.class;
    }

	@Override
	public StreamCode streamCommandService(TBase tBase, ServerStreamChannelContext streamChannelContext) {
		logger.info("streamCommandService object:{}, streamChannelContext:{}", tBase, streamChannelContext);
		setRequesttbase(tBase);
		TCommandThreadDump param = (TCommandThreadDump) tBase;
		setRunnum(param.getNum());
		streamChannelContext.getStreamChannel().addStateChangeEventHandler(stateChangeEventHandler);
		return StreamCode.OK;
	}
	
	
	public int getRunnum() {
		return runnum;
	}

	public void setRunnum(int runnum) {
		this.runnum = runnum;
	}

	public TBase getRequesttbase() {
		return requesttbase;
	}

	public void setRequesttbase(TBase requesttbase) {
		this.requesttbase = requesttbase;
	}


	private class ThreadDumpStreamChannelStateChangeEventHandler implements StreamChannelStateChangeEventHandler<ServerStreamChannel>{

		@Override
		public void eventPerformed(ServerStreamChannel streamChannel, StreamChannelStateCode updatedStateCode)
				throws Exception {
			  logger.info("eventPerformed. ServerStreamChannel:{}, StreamChannelStateCode:{}.", streamChannel, updatedStateCode);
	            synchronized (lock) {
	                switch (updatedStateCode) {
	                    case CONNECTED:
	                        streamChannelRepository.add(streamChannel);
	                        boolean turnOn = onTimerTask.compareAndSet(false, true);
	                        if (turnOn) {
	                            logger.info("turn on ActiveThreadCountTimerTask.");
	                            timer.newTimeout(new ThreadDumpTimerTask(), flushDelay, TimeUnit.MILLISECONDS);
	                        }
	                        break;
	                    case CLOSED:
	                    case ILLEGAL_STATE:
	                        boolean removed = streamChannelRepository.remove(streamChannel);
	                        if (removed && streamChannelRepository.isEmpty()) {
	                            boolean turnOff = onTimerTask.compareAndSet(true, false);
	                            if (turnOff) {
	                                logger.info("turn off ActiveThreadCountTimerTask.");
	                            }
	                        }
	                        break;
	                }
	            }
			
		}

		@Override
		public void exceptionCaught(ServerStreamChannel streamChannel, StreamChannelStateCode updatedStateCode,
				Throwable e) {
			 logger.warn("exceptionCaught caused:{}. ServerStreamChannel:{}, StreamChannelStateCode:{}.", e.getMessage(), streamChannel, updatedStateCode, e);
		}
		
	}
	
	private class ThreadDumpTimerTask implements TimerTask {

		@Override
		public void run(Timeout timeout) throws Exception {
			logger.debug("ThreadDumpTimerTask started. target-streams:{}", streamChannelRepository);
			try {
				TBase returnTbase = requestCommandService(getRequesttbase());
				byte[] payload = SerializationUtils.serialize(returnTbase, CommandSerializer.SERIALIZER_FACTORY, null);
				if (payload != null) {
					byte[] newpayload = Snappy.compress(payload);
					logger.info("payload,{},{}, {}", payload.length, newpayload.length);
					for (ServerStreamChannel serverStreamChannel : streamChannelRepository) {
						serverStreamChannel.sendData(newpayload);
					}
				}
			} finally {
				if (timer != null && onTimerTask.get()) {
					setRunnum(getRunnum() - 1);
					if (getRunnum() <= 0) {
						boolean turnOff = onTimerTask.compareAndSet(true, false);
						if (turnOff) {
							logger.info("runnum turn off ActiveThreadCountTimerTask.");
						}
					}else{
						timer.newTimeout(this, flushDelay, TimeUnit.MILLISECONDS);
					}
				}
			}
		}

	}

}
